/**
 * AGNModelCanvas.java
 */

package agngui.agnmodel;
import java.awt.Color;
import java.awt.Font;
import java.awt.GraphicsConfiguration;
import java.util.HashMap;
import java.util.Map;

import javax.media.j3d.Canvas3D;
import javax.media.j3d.J3DGraphics2D;
import javax.media.j3d.Transform3D;
import javax.vecmath.AxisAngle4d;
import javax.vecmath.Point2d;
import javax.vecmath.Point2i;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;

import agngui.event.AGNEvent;
import agngui.event.AGNListener;
import agngui.event.HandleDragEvent;
import agngui.event.HandleEvent;
import agngui.event.HandleReleaseEvent;
import agngui.event.HandleSelectEvent;
import agngui.event.ObjectEvent;
import agngui.event.ObjectReleaseEvent;
import agngui.event.ObjectSelectEvent;
import agngui.ui.Field;

/**
 * Basically allows for HUD-like rendering and capabilities for the AGN
 * model renderer.
 */
public class AGNModelCanvas extends Canvas3D implements AGNListener {
	
	/**
	 * The color for a control axis
	 */
    private static Color  CONTROL_AXIS_COLOR    = Color.GREEN;
    
    /**
     * The color of a HUD when it is selected and being dragged
     */
    private static Color  SELECT_COLOR     = Color.RED;
    
    /**
     * The color for the linear HUD handles
     */
    private static Color  LINEAR_COLOR     = CONTROL_AXIS_COLOR;
    
    /**
     * The color for the angular HUD handles
     */
    private static Color  ANGULAR_COLOR    = Color.CYAN;
    
    /**
     * The color for the rotational HUD handles
     */
    private static Color  ROTATIONAL_COLOR = Color.YELLOW;
    
    /**
     * The width of the HUD rectangle in pixels
     */
    static int RECT_WIDTH       = 20;
    
    /**
     * The height of the HUD rectangle in pixels
     */
    static int RECT_HEIGHT      = 20;
    
    

    /**
     * the object that is currently being worked on in the system
     */
    private AGNObject          activeObject;
    
    /**
     * the graphics handle that helps update the Canvas3D
     */
    private J3DGraphics2D      graphics;
    
    /**
     * the point in world space which is selected
     */
    private Point3d            selectPoint;
    
    /**
     * The currently selected handle
     */
    private AGNParameterHandle selectHandle;

    /**    
     * AGNModelCanvas
     *
     * Initializes the canvas.
     *
     * @param config The GraphicsConfiguration with which to initialize the Canvas3D
     */
    public AGNModelCanvas( GraphicsConfiguration config ) {
        super( config );
        graphics = getGraphics2D();
        graphics.setFont( new Font( "SansSerif", Font.BOLD, 20 ) );
    }
    
    /**
     * Processes events in the system that are specific to the canvas
     */
    public void processEvent(AGNEvent event) {

    	if(event instanceof ObjectEvent) {
    		if(event instanceof ObjectSelectEvent) {
    			setActiveObject(((ObjectSelectEvent)event).getObject());
    		} else if(event instanceof ObjectReleaseEvent) {
    			setActiveObject(null);
    		}
    	} else if(event instanceof HandleEvent) {
    		AGNParameterHandle handle = ((HandleEvent)event).getHandle();
    		if(event instanceof HandleSelectEvent) {
    			//need the thing to scale so that it doesn't drop to zero
    			handle.calculateNewValueFromWorldLocation(handle.getPoint());
    			setSelectPoint(handle, handle.getPoint());
	    	} else if(event instanceof HandleDragEvent) {
	    		Point3d point = ((HandleDragEvent)event).getPoint();
	    		handle.calculateNewValueFromWorldLocation( point );
	    		setSelectPoint(handle, point);
	    	} else if(event instanceof HandleReleaseEvent) {
	    		setSelectPoint(null, null);
	    	}
    	}
    }

    /**
     * setActiveObject
     *
     * Sets an AGNObject as the "active" object-- meaning that it is 
     * currently selected in the HUD and the UI. Also repaints the 
     * canvas.
     *
     * @param object An AGNObject reference of the object to the object to
     *               make active.
     */
    private void setActiveObject( AGNObject object ) {
        activeObject = object;
        paint(getGraphics());
    }

    /**
     * setSelectPoint
     *
     * Sets an active select point (the point where a handle is being dragged)
     *
     * @param handle A reference to the handle currently being dragged in the HUD.
     * @param point The current world-space projection of the dragged handle location.
     */
    public void setSelectPoint( AGNParameterHandle handle, Point3d point ) {
        String handleStatus = new String();
        String units        = new String();
        Double recentValue;
      
        if( handle != null ) {
        	if(handle instanceof AGNAngularParameterHandle || handle instanceof AGNRotationalParameterHandle) {
        		recentValue = new Double( handle.getRecentValue()*180/Math.PI );
                units = "degrees";
        	} else {
        		recentValue = new Double( handle.getRecentValue() );
                units = "meters";
        	}
          String val = Field.large_df.format(recentValue);
          if(recentValue < 1e8) {
        	  val = Field.small_df.format(recentValue);
          }
          handleStatus = handle.getIdentifier() + val;

          graphics.setColor( Color.WHITE );
          graphics.drawString( handleStatus + " " + units, 0, 20 ); 
        }

        selectHandle = handle;
        selectPoint  = point;

        paint(graphics);
    }   

    /**
     * implemented method of Canvas3D, tells the canvas what to do to set up for
     * rendering
     */
    public void preRender() {
        if( activeObject != null ) {
            AGNParameterHandle handles[] = activeObject.getHandles();
            Point3d point;
            Point2i pixel;
			
			Point3d[] point_array = activeObject.getPointArray();
			Vector3f[] norm_array = activeObject.getNormArray();
			if(point_array != null && norm_array != null) {
				//drawVectorsFromPoints(point_array, norm_array);
			}
            
            // Draw unselected handles
            for(AGNParameterHandle handle : handles) {
                int handleType = handle.getType();

                point = handle.getPoint();
                pixel = getScreenLocationFromWorldSpace( point );

                switch( handleType ) {
                    case AGNParameterHandle.TYPE_ROTATIONAL:
                        AGNRotationalParameterHandle rotHandle = (AGNRotationalParameterHandle)handle;
                        drawRotationalHelperGeometry( rotHandle, ROTATIONAL_COLOR );
                        drawRectAroundPixel( pixel, ROTATIONAL_COLOR );
                        break;
                    case AGNParameterHandle.TYPE_LINEAR:
                        drawRectAroundPixel( pixel, LINEAR_COLOR );
                        break;
                    case AGNParameterHandle.TYPE_ANGULAR:
                        drawRectAroundPixel( pixel, ANGULAR_COLOR );
                        break;
                }
            }

            // Draw the selected/dragging handle
            if( selectPoint != null && selectHandle != null ) {
                int handleType = selectHandle.getType();

                pixel = getScreenLocationFromWorldSpace( selectPoint );
                drawRectAroundPixel( pixel, SELECT_COLOR );

                // Draw selected handle "helpers"
                switch( handleType ) {
                    case AGNParameterHandle.TYPE_ANGULAR:
                        AGNAngularParameterHandle angleHandle = (AGNAngularParameterHandle)selectHandle;
                        drawAngularHelperGeometry( angleHandle, selectPoint, ANGULAR_COLOR );
                    break;
                    case AGNParameterHandle.TYPE_LINEAR:
                        AGNLinearParameterHandle linearHandle = (AGNLinearParameterHandle)selectHandle;
                        drawLinearHelperGeometry( linearHandle, selectPoint, LINEAR_COLOR );
                    break;
                    case AGNParameterHandle.TYPE_ROTATIONAL:
                        AGNRotationalParameterHandle rotHandle = (AGNRotationalParameterHandle)selectHandle;
                        drawRotationalHelperGeometry( rotHandle, selectPoint, ROTATIONAL_COLOR );
                    break;
                }
            } 
        }
    }

    /**
     * tells the canvas what to do after the image has been created
     */
    public void postRender() {
        graphics.flush(false);
    }
    
    /**
     * getScreenLocationFromWorldSpace
     *
     * Gets the screen location that a world point corresponds to (an
     * inverse projection, if you will).
     *
     * @param _worldPoint A point in world-space corresponding to a
     *                    point in screen coordinates.
     * @return the screen point that _worldPoint corresponds to.
     */
    public Point2i getScreenLocationFromWorldSpace( Point3d _worldPoint ) {
        Transform3D worldTransform = new Transform3D();
        Point2d     temp           = new Point2d();
        Point2i     retVal         = null;
        Point3d     worldPoint     = new Point3d(_worldPoint);
        
        // Retrieve the world->image plate transform
        getVworldToImagePlate( worldTransform );

        // Project point into pixel space
        worldTransform.transform( worldPoint );
        getPixelLocationFromImagePlate( worldPoint, temp );

        retVal = new Point2i( (int)temp.x, (int)temp.y );

        return retVal;
    }

    /**
     * getScreenLocationsFromWorldSpace
     *
     * Gets the screen coordinate locations for an array of world points. 
     *
     * @param _worldPoints the array of world point, each element of which we aim
     *                     to calculate the corresponding screen space coordinate.
     * @return an array of screen coordinates corresponding to the _worldPoint indeces
     */
    public Point2i[] getScreenLocationsFromWorldSpace( Point3d[] _worldPoints ) {
        Transform3D worldTransform = new Transform3D();
        Point2d     temp           = new Point2d();
        Point2i     retVal[]       = new Point2i[_worldPoints.length];
        Point3d     worldPoints[]  = new Point3d[_worldPoints.length];

        System.arraycopy( _worldPoints, 0, worldPoints, 0, _worldPoints.length );

        // Retrieve the world->imageplate transform
        getVworldToImagePlate( worldTransform );

        // Project points into pixel space
        for( int i = 0; i < worldPoints.length; ++i ) {
            worldTransform.transform( worldPoints[i] ); 
            getPixelLocationFromImagePlate( worldPoints[i], temp );

            retVal[i] = new Point2i( (int)temp.x, (int)temp.y );
        }

        return retVal;
    }

    /**
     * getSelectPoint
     *
     * Gets the currently selected point
     *
     * @return A reference to the currently selected point
     */
    public Point3d getSelectPoint() {
        return selectPoint;
    }

    /**
     * drawRectAroundPixel
     *
     * Draws a rectangle of a given color around a pixel.
     *
     * @param point The pixel around which to draw a rectangle on the canvas
     * @param color The color of the rectangle to draw
     */
    private void drawRectAroundPixel( Point2i point, Color color ) {
        int x = point.x - RECT_WIDTH  / 2;
        int y = point.y - RECT_HEIGHT / 2;

        graphics.setColor( color );
        graphics.drawRect( x, y, RECT_WIDTH, RECT_HEIGHT );
    }

    /**
     * drawCircleAroundPoint
     *
     * Draws a circle in 3-space around a given 3D point.
     *
     * @param center The center point in world space of the circle to draw
     * @param start  The "start point" for drawing the circle
     * @param radius The radius, in world space, of the circle to draw
     * @param sides  The number of sides to use in drawing the circle
     * @param color  The color to use in drawing the circle
     */
    private void drawCircleAroundPoint( Point3d center, Point3d start, double radius,
                                        int sides, Color color ) {
        AxisAngle4d axisAngle    = new AxisAngle4d();
        Transform3D transform    = new Transform3D();
        Point3d     points[]     = new Point3d[sides];
        Point2i     pixel;
        Vector3d    centerVector = new Vector3d( center );
        double      dTheta       = (2 * Math.PI) / sides;
        int         xCoords[]    = new int[sides];
        int         yCoords[]    = new int[sides];

        // Get the 3D coordinates for the circle
        int i = 0;
        for( double theta = 0; theta < (2 * Math.PI); theta += dTheta, ++i ) {
            axisAngle.set( centerVector, theta );
            transform.setRotation( axisAngle );
            points[i] = new Point3d( start );

            transform.transform( points[i] );
            pixel = getScreenLocationFromWorldSpace( points[i] );
            xCoords[i] = pixel.x;
            yCoords[i] = pixel.y;
        }

        graphics.setColor( color );
        graphics.drawPolygon( xCoords, yCoords, sides );
    }

    /**
     * drawVectorsFromPoints
     *
     * Draws vectors from points. Mostly for debugging purposes.
     *
     * @param points  An array of points from which to draw vectors.
     * @param vectors An array of vectors whose indeces correspond to the points
     *                from which to draw.
     */
    public void drawVectorsFromPoints( Point3d[] points, Vector3f[] vectors ) {
		if( points.length != vectors.length ) {
            throw new RuntimeException( "drawVectorsFromPoints-- invalid parameters" );
        }
		
		//we don't want to draw the same (point, vector) pair more than once, so reduce them using a map.
		Map<Point3d, Point3d> unique_points = new HashMap<Point3d, Point3d>();
		for(int i = 0; i < points.length; i++) {
			unique_points.put(points[i], new Point3d( vectors[i] ));
		}

		for(Map.Entry<Point3d, Point3d> entry : unique_points.entrySet()) {
			Point3d origin_point = new Point3d(entry.getKey());
			//the origin points are not scaled down, so they're still light-years across, we want them to be only a few units across.
			origin_point.scale(AGNObject.SCALE_FACTOR);
			
			//adding the origin point to the vector will make a line segment that we can draw from the origin along the vector
			//and make a nice viewable representation of the vector
			Point3d vector = new Point3d(entry.getValue());
			vector.add(origin_point);
			
			Point2i op = getScreenLocationFromWorldSpace(origin_point);
			Point2i vec = getScreenLocationFromWorldSpace(vector);
            graphics.setColor( CONTROL_AXIS_COLOR );
			graphics.drawLine( op.x, op.y, vec.x, vec.y );
		}
    }

    /**
     * drawLineOnAxis
     *
     * Traces an axis-- draws a line over it!
     *
     * @param axis  The axis to trace over
     * @param color The color of the line tracing the "axis" vector
     */
    private void drawLineOnAxis( Vector3d axis, Color color ) {
        Point2i  centerPoint;
        Point2i  endPoint;

        centerPoint = getScreenLocationFromWorldSpace( new Point3d() );
        endPoint    = getScreenLocationFromWorldSpace( new Point3d( axis ) );    

        graphics.setColor( color );
        graphics.drawLine( centerPoint.x, centerPoint.y, endPoint.x, endPoint.y );
    }

    /**
     * drawLinearHelperGeometry
     *
     * Draw the helper geometry for a linear parameter handle.
     * 
     * @param handle The handle for which we're drawing helper geometry
     * @param point  The currently selected point in the HUD
     * @param color  The color of the helper geometry we're going to draw.
     */
    private void drawLinearHelperGeometry( AGNLinearParameterHandle handle, Point3d point, Color color ) {
        Point3d     center      = new Point3d(); // The origin
        Point2i     centerPoint;
        Point2i     handlePoint;

        // Convert points to pixels
        centerPoint = getScreenLocationFromWorldSpace( center );
        handlePoint = getScreenLocationFromWorldSpace( point  );

        // Draw lines
        graphics.setColor( color );
        graphics.drawLine( centerPoint.x, centerPoint.y, handlePoint.x, handlePoint.y );
    }

    /**
     * drawAngularHelperGeometry
     *
     * Draw helper geometry for angular parameter handles.
     *
     * @param handle The angular handle for which to draw helper geometry
     * @param point  The currently selected point for "handle"
     * @param color  The color used to draw the helper goemetry
     */
    private void drawAngularHelperGeometry( AGNAngularParameterHandle handle, Point3d point, Color color ) {
        AGNObject   object      = handle.getObject();
        Transform3D transform   = new Transform3D(); 
        Vector3d    up          = new Vector3d( 0.0, 0.0, 1.0 );
        Vector3d    pointVector = new Vector3d( point );
        Point3d     center      = new Point3d(); // The origin
        Point2i     centerPoint;
        Point2i     handlePoint;
        Point2i     upPoint;       

        transform = object.getFullTransform(); //object.getTransformGroup().getTransform( transform );

        transform.transform( up ); // Transform up vector from object space to world space
        up.normalize();
        up.scale( pointVector.length() );

        // Convert points to screen space
        centerPoint = getScreenLocationFromWorldSpace( center );
        handlePoint = getScreenLocationFromWorldSpace( new Point3d( pointVector ) );
        upPoint     = getScreenLocationFromWorldSpace( new Point3d( up ) );

        // Draw lines
        graphics.setColor( color );
        graphics.drawLine( centerPoint.x, centerPoint.y, upPoint.x, upPoint.y );
        graphics.drawLine( centerPoint.x, centerPoint.y, handlePoint.x, handlePoint.y );
    }

    /**
     * drawRotationalHelperGeometry
     *
     * Draw helper geometry for a rotational handle.
     *
     * @param rotHandle A reference to a rotational handle
     * @param color     The color in which to draw the helper geometry
     */
    private void drawRotationalHelperGeometry( AGNRotationalParameterHandle rotHandle, Color color ) {
        double radius                          = rotHandle.getRadius();

        drawLineOnAxis( new Vector3d( rotHandle.getCenterPoint() ), color );
        drawCircleAroundPoint( rotHandle.getCenterPoint(), 
                               rotHandle.getPoint(),
                               radius, 
                               20,
                               color );
    }

    /**
     * drawLine
     *
     * Draws a line from one point to another.
     *
     * @param p1 The starting point for the line to draw
     * @param p2 The end point for the line to draw
     */
    public void drawLine( Point3d p1, Point3d p2 ) {
        Point2i pixel1, pixel2;

        pixel1 = getScreenLocationFromWorldSpace( p1 );
        pixel2 = getScreenLocationFromWorldSpace( p2 );

        graphics.drawLine( pixel1.x, pixel1.y, pixel2.x, pixel2.y );
    }

    /**
     * drawRotationalHelperGeometry
     *
     * Draws helper geometry for an ACTIVE rotational handle.
     *
     * @param rotHandle   The currently active rotational handle
     * @param selectPoint The currently selected point the handle has been dragged to
     * @param color       The color in which to draw the helper geometry
     */
    private void drawRotationalHelperGeometry( AGNRotationalParameterHandle rotHandle, Point3d selectPoint, 
                                               Color color ) {
        Point3d centerPoint = rotHandle.getCenterPoint();
        Point3d handlePoint = rotHandle.getPoint();
        Point2i pixels[]    = new Point2i[3];

        pixels[0] = getScreenLocationFromWorldSpace( handlePoint ); // The original handle position
        pixels[1] = getScreenLocationFromWorldSpace( centerPoint ); // The center position of the circle
        pixels[2] = getScreenLocationFromWorldSpace( selectPoint ); // the currently selected position

        graphics.setColor( color );
        graphics.drawLine( pixels[0].x, pixels[0].y, pixels[1].x, pixels[1].y );
        graphics.drawLine( pixels[1].x, pixels[1].y, pixels[2].x, pixels[2].y );
    }
    
    


	/**
	 * gives back the control axis color
	 * @return the control axis color
	 */
	public static Color getControlAxisColor() {
		return CONTROL_AXIS_COLOR;
	}

	/**
	 * sets the control axis color
	 * @param control_axis_color the control axis color to set
	 */
	public static void setControlAxisColor(Color control_axis_color) {
		CONTROL_AXIS_COLOR = control_axis_color;
	}

	/**
	 * gives back the HUD select color
	 * @return the HUD select color
	 */
	public static Color getSelectColor() {
		return SELECT_COLOR;
	}

	/**
	 * sets the HUD select color
	 * @param select_color the HUD select color to set
	 */
	public static void setSelectColor(Color select_color) {
		SELECT_COLOR = select_color;
	}

	/**
	 * gives back the linear HUD color
	 * @return the linear HUD color
	 */
	public static Color getLinearColor() {
		return LINEAR_COLOR;
	}

	/**
	 * sets the linear HUD color
	 * @param linear_color the linear HUD color to set
	 */
	public static void setLinearColor(Color linear_color) {
		LINEAR_COLOR = linear_color;
	}

	/**
	 * sets the angular HUD color
	 * @return the angular HUD color
	 */
	public static Color getAngularColor() {
		return ANGULAR_COLOR;
	}

	/**
	 * sets the angular HUD color
	 * 
	 * @param angular_color the angular HUD color to set
	 */
	public static void setAngularColor(Color angular_color) {
		ANGULAR_COLOR = angular_color;
	}

	/**
	 * gives back the rotational HUD color
	 * 
	 * @return the rotational HUD color
	 */
	public static Color getRotationalColor() {
		return ROTATIONAL_COLOR;
	}

	/**
	 * sets the rotational HUD color
	 * 
	 * @param rotational_color the rotational HUD color to set
	 */
	public static void setRotationalColor(Color rotational_color) {
		ROTATIONAL_COLOR = rotational_color;
	}

}
